/**
 * Copyright (c) 广州小橘灯信息科技有限公司 2016-2017, wjun_java@163.com.
 * <p>
 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl-3.0.txt
 * http://www.xjd2020.com
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baomidou.mybatisplus.core;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.fastcms.mybatis.IgnoreDataPermission;
import com.fastcms.mybatis.IgnoreDataPermissionCache;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.CacheRefResolver;
import org.apache.ibatis.builder.IncompleteElementException;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.annotation.MapperAnnotationBuilder;
import org.apache.ibatis.builder.annotation.MethodResolver;
import org.apache.ibatis.builder.annotation.ProviderSqlSource;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.parsing.PropertyParser;
import org.apache.ibatis.reflection.TypeParameterResolver;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.UnknownTypeHandler;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 覆盖mybatis-plus
 * 再解析mapper class method的时候追加过滤掉数据权限的注解
 * @author： wjun_java@163.com
 * @date： 2022/9/25
 * @description：
 * @modifiedBy：
 * @version: 1.0
 */
public class MybatisMapperAnnotationBuilder extends MapperAnnotationBuilder {

	private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
			.of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
					InsertProvider.class, DeleteProvider.class)
			.collect(Collectors.toSet());

	private final Configuration configuration;
	private final MapperBuilderAssistant assistant;
	private final Class<?> type;

	public MybatisMapperAnnotationBuilder(Configuration configuration, Class<?> type) {
		super(configuration, type);
		String resource = type.getName().replace(StringPool.DOT, StringPool.SLASH) + ".java (best guess)";
		this.assistant = new MapperBuilderAssistant(configuration, resource);
		this.configuration = configuration;
		this.type = type;
	}

	@Override
	public void parse() {
		String resource = type.toString();
		if (!configuration.isResourceLoaded(resource)) {
			loadXmlResource();
			configuration.addLoadedResource(resource);
			String mapperName = type.getName();
			assistant.setCurrentNamespace(mapperName);
			parseCache();
			parseCacheRef();
			InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
			for (Method method : type.getMethods()) {
				if (!canHaveStatement(method)) {
					continue;
				}
				if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
						&& method.getAnnotation(ResultMap.class) == null) {
					parseResultMap(method);
				}
				try {

					if (method.getAnnotation(IgnoreDataPermission.class) != null) {
						// 如果标记有忽略sql权限的注解
						if (method.getParameterTypes() != null
								&& Arrays.stream(method.getParameterTypes()).filter(item -> IPage.class.isAssignableFrom(item)).collect(Collectors.toList()).size() > 0) {
							IgnoreDataPermissionCache.add(mapperName.concat(StringPool.DOT).concat(method.getName()).concat("_mpCount"));
						}
						IgnoreDataPermissionCache.add(mapperName.concat(StringPool.DOT).concat(method.getName()));
					}

					// TODO 加入 注解过滤缓存
					InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
					parseStatement(method);
				} catch (IncompleteElementException e) {
					// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
					configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
				}
			}
			// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
			try {
				// https://github.com/baomidou/mybatis-plus/issues/3038
				if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
					parserInjector();
				}
			} catch (IncompleteElementException e) {
				configuration.addIncompleteMethod(new InjectorResolver(this));
			}
		}
		parsePendingMethods();
	}

	void parserInjector() {
		GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
	}

	private boolean canHaveStatement(Method method) {
		// issue #237
		return !method.isBridge() && !method.isDefault();
	}

	private void parsePendingMethods() {
		Collection<MethodResolver> incompleteMethods = configuration.getIncompleteMethods();
		synchronized (incompleteMethods) {
			Iterator<MethodResolver> iter = incompleteMethods.iterator();
			while (iter.hasNext()) {
				try {
					iter.next().resolve();
					iter.remove();
				} catch (IncompleteElementException e) {
					// This method is still missing a resource
				}
			}
		}
	}

	private void loadXmlResource() {
		// Spring may not know the real resource name so we check a flag
		// to prevent loading again a resource twice
		// this flag is set at XMLMapperBuilder#bindMapperForNamespace
		if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
			String xmlResource = type.getName().replace(StringPool.DOT, StringPool.SLASH) + ".xml";
			// #1347
			InputStream inputStream = type.getResourceAsStream(StringPool.SLASH + xmlResource);
			if (inputStream == null) {
				// Search XML mapper that is not in the module but in the classpath.
				try {
					inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
				} catch (IOException e2) {
					// ignore, resource is not required
				}
			}
			if (inputStream != null) {
				XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
				xmlParser.parse();
			}
		}
	}

	private void parseCache() {
		CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
		if (cacheDomain != null) {
			Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
			Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
			Properties props = convertToProperties(cacheDomain.properties());
			assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
		}
	}

	private Properties convertToProperties(Property[] properties) {
		if (properties.length == 0) {
			return null;
		}
		Properties props = new Properties();
		for (Property property : properties) {
			props.setProperty(property.name(),
					PropertyParser.parse(property.value(), configuration.getVariables()));
		}
		return props;
	}

	private void parseCacheRef() {
		CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
		if (cacheDomainRef != null) {
			Class<?> refType = cacheDomainRef.value();
			String refName = cacheDomainRef.name();
			if (refType == void.class && refName.isEmpty()) {
				throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
			}
			if (refType != void.class && !refName.isEmpty()) {
				throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
			}
			String namespace = (refType != void.class) ? refType.getName() : refName;
			try {
				assistant.useCacheRef(namespace);
			} catch (IncompleteElementException e) {
				configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
			}
		}
	}

	private String parseResultMap(Method method) {
		Class<?> returnType = getReturnType(method);
		Arg[] args = method.getAnnotationsByType(Arg.class);
		Result[] results = method.getAnnotationsByType(Result.class);
		TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
		String resultMapId = generateResultMapName(method);
		applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
		return resultMapId;
	}

	private String generateResultMapName(Method method) {
		Results results = method.getAnnotation(Results.class);
		if (results != null && !results.id().isEmpty()) {
			return type.getName() + StringPool.DOT + results.id();
		}
		StringBuilder suffix = new StringBuilder();
		for (Class<?> c : method.getParameterTypes()) {
			suffix.append(StringPool.DASH);
			suffix.append(c.getSimpleName());
		}
		if (suffix.length() < 1) {
			suffix.append("-void");
		}
		return type.getName() + StringPool.DOT + method.getName() + suffix;
	}

	private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results, TypeDiscriminator discriminator) {
		List<ResultMapping> resultMappings = new ArrayList<>();
		applyConstructorArgs(args, returnType, resultMappings);
		applyResults(results, returnType, resultMappings);
		Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
		// TODO add AutoMappingBehaviour
		assistant.addResultMap(resultMapId, returnType, null, disc, resultMappings, null);
		createDiscriminatorResultMaps(resultMapId, returnType, discriminator);
	}

	private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
		if (discriminator != null) {
			for (Case c : discriminator.cases()) {
				String caseResultMapId = resultMapId + StringPool.DASH + c.value();
				List<ResultMapping> resultMappings = new ArrayList<>();
				// issue #136
				applyConstructorArgs(c.constructArgs(), resultType, resultMappings);
				applyResults(c.results(), resultType, resultMappings);
				// TODO add AutoMappingBehaviour
				assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
			}
		}
	}

	private Discriminator applyDiscriminator(String resultMapId, Class<?> resultType, TypeDiscriminator discriminator) {
		if (discriminator != null) {
			String column = discriminator.column();
			Class<?> javaType = discriminator.javaType() == void.class ? String.class : discriminator.javaType();
			JdbcType jdbcType = discriminator.jdbcType() == JdbcType.UNDEFINED ? null : discriminator.jdbcType();
			@SuppressWarnings("unchecked")
			Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
					(discriminator.typeHandler() == UnknownTypeHandler.class ? null : discriminator.typeHandler());
			Case[] cases = discriminator.cases();
			Map<String, String> discriminatorMap = new HashMap<>();
			for (Case c : cases) {
				String value = c.value();
				String caseResultMapId = resultMapId + StringPool.DASH + value;
				discriminatorMap.put(value, caseResultMapId);
			}
			return assistant.buildDiscriminator(resultType, column, javaType, jdbcType, typeHandler, discriminatorMap);
		}
		return null;
	}

	void parseStatement(Method method) {
		final Class<?> parameterTypeClass = getParameterType(method);
		final LanguageDriver languageDriver = getLanguageDriver(method);

		getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
			final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method);
			final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
			final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options) x.getAnnotation()).orElse(null);
			final String mappedStatementId = type.getName() + StringPool.DOT + method.getName();

			final KeyGenerator keyGenerator;
			String keyProperty = null;
			String keyColumn = null;
			if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
				// first check for SelectKey annotation - that overrides everything else
				SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey) x.getAnnotation()).orElse(null);
				if (selectKey != null) {
					keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
					keyProperty = selectKey.keyProperty();
				} else if (options == null) {
					keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
				} else {
					keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
					keyProperty = options.keyProperty();
					keyColumn = options.keyColumn();
				}
			} else {
				keyGenerator = NoKeyGenerator.INSTANCE;
			}

			Integer fetchSize = null;
			Integer timeout = null;
			StatementType statementType = StatementType.PREPARED;
			ResultSetType resultSetType = configuration.getDefaultResultSetType();
			boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
			boolean flushCache = !isSelect;
			boolean useCache = isSelect;
			if (options != null) {
				if (Options.FlushCachePolicy.TRUE.equals(options.flushCache())) {
					flushCache = true;
				} else if (Options.FlushCachePolicy.FALSE.equals(options.flushCache())) {
					flushCache = false;
				}
				useCache = options.useCache();
				fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
				timeout = options.timeout() > -1 ? options.timeout() : null;
				statementType = options.statementType();
				if (options.resultSetType() != ResultSetType.DEFAULT) {
					resultSetType = options.resultSetType();
				}
			}

			String resultMapId = null;
			if (isSelect) {
				ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
				if (resultMapAnnotation != null) {
					resultMapId = String.join(StringPool.COMMA, resultMapAnnotation.value());
				} else {
					resultMapId = generateResultMapName(method);
				}
			}

			assistant.addMappedStatement(
					mappedStatementId,
					sqlSource,
					statementType,
					sqlCommandType,
					fetchSize,
					timeout,
					// ParameterMapID
					null,
					parameterTypeClass,
					resultMapId,
					getReturnType(method),
					resultSetType,
					flushCache,
					useCache,
					// TODO gcode issue #577
					false,
					keyGenerator,
					keyProperty,
					keyColumn,
					statementAnnotation.getDatabaseId(),
					languageDriver,
					// ResultSets
					options != null ? nullOrEmpty(options.resultSets()) : null);
		});
	}

	private LanguageDriver getLanguageDriver(Method method) {
		Lang lang = method.getAnnotation(Lang.class);
		Class<? extends LanguageDriver> langClass = null;
		if (lang != null) {
			langClass = lang.value();
		}
		return configuration.getLanguageDriver(langClass);
	}

	private Class<?> getParameterType(Method method) {
		Class<?> parameterType = null;
		Class<?>[] parameterTypes = method.getParameterTypes();
		for (Class<?> currentParameterType : parameterTypes) {
			if (!RowBounds.class.isAssignableFrom(currentParameterType) && !ResultHandler.class.isAssignableFrom(currentParameterType)) {
				if (parameterType == null) {
					parameterType = currentParameterType;
				} else {
					// issue #135
					parameterType = MapperMethod.ParamMap.class;
				}
			}
		}
		return parameterType;
	}

	private Class<?> getReturnType(Method method) {
		Class<?> returnType = method.getReturnType();
		Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, type);
		if (resolvedReturnType instanceof Class) {
			returnType = (Class<?>) resolvedReturnType;
			if (returnType.isArray()) {
				returnType = returnType.getComponentType();
			}
			// gcode issue #508
			if (void.class.equals(returnType)) {
				ResultType rt = method.getAnnotation(ResultType.class);
				if (rt != null) {
					returnType = rt.value();
				}
			}
		} else if (resolvedReturnType instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) resolvedReturnType;
			Class<?> rawType = (Class<?>) parameterizedType.getRawType();
			if (Collection.class.isAssignableFrom(rawType) || Cursor.class.isAssignableFrom(rawType)) {
				Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
				if (actualTypeArguments != null && actualTypeArguments.length == 1) {
					Type returnTypeParameter = actualTypeArguments[0];
					if (returnTypeParameter instanceof Class<?>) {
						returnType = (Class<?>) returnTypeParameter;
					} else if (returnTypeParameter instanceof ParameterizedType) {
						// (gcode issue #443) actual type can be a also a parameterized type
						returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
					} else if (returnTypeParameter instanceof GenericArrayType) {
						Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();
						// (gcode issue #525) support List<byte[]>
						returnType = Array.newInstance(componentType, 0).getClass();
					}
				}
			} else if (method.isAnnotationPresent(MapKey.class) && Map.class.isAssignableFrom(rawType)) {
				// (gcode issue 504) Do not look into Maps if there is not MapKey annotation
				Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
				if (actualTypeArguments != null && actualTypeArguments.length == 2) {
					Type returnTypeParameter = actualTypeArguments[1];
					if (returnTypeParameter instanceof Class<?>) {
						returnType = (Class<?>) returnTypeParameter;
					} else if (returnTypeParameter instanceof ParameterizedType) {
						// (gcode issue 443) actual type can be a also a parameterized type
						returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
					}
				}
			} else if (Optional.class.equals(rawType)) {
				Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
				Type returnTypeParameter = actualTypeArguments[0];
				if (returnTypeParameter instanceof Class<?>) {
					returnType = (Class<?>) returnTypeParameter;
				}
			}
			// TODO 下面是支援 IPage 及其子类作为返回值的
			else if (IPage.class.isAssignableFrom(rawType)) {
				Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
				Type returnTypeParameter = actualTypeArguments[0];
				if (returnTypeParameter instanceof Class<?>) {
					returnType = (Class<?>) returnTypeParameter;
				} else if (returnTypeParameter instanceof ParameterizedType) {
					returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();
				}
			}
			// TODO 上面是支援 IPage 及其子类作为返回值的
		}

		return returnType;
	}

	private void applyResults(Result[] results, Class<?> resultType, List<ResultMapping> resultMappings) {
		for (Result result : results) {
			List<ResultFlag> flags = new ArrayList<>();
			if (result.id()) {
				flags.add(ResultFlag.ID);
			}
			@SuppressWarnings("unchecked")
			Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
					((result.typeHandler() == UnknownTypeHandler.class) ? null : result.typeHandler());
			boolean hasNestedResultMap = hasNestedResultMap(result);
			ResultMapping resultMapping = assistant.buildResultMapping(
					resultType,
					nullOrEmpty(result.property()),
					nullOrEmpty(result.column()),
					result.javaType() == void.class ? null : result.javaType(),
					result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),
					hasNestedSelect(result) ? nestedSelectId(result) : null,
					hasNestedResultMap ? nestedResultMapId(result) : null,
					null,
					hasNestedResultMap ? findColumnPrefix(result) : null,
					typeHandler,
					flags,
					null,
					null,
					isLazy(result));
			resultMappings.add(resultMapping);
		}
	}

	private String findColumnPrefix(Result result) {
		String columnPrefix = result.one().columnPrefix();
		if (columnPrefix.length() < 1) {
			columnPrefix = result.many().columnPrefix();
		}
		return columnPrefix;
	}

	private String nestedResultMapId(Result result) {
		String resultMapId = result.one().resultMap();
		if (resultMapId.length() < 1) {
			resultMapId = result.many().resultMap();
		}
		if (!resultMapId.contains(StringPool.DOT)) {
			resultMapId = type.getName() + StringPool.DOT + resultMapId;
		}
		return resultMapId;
	}

	private boolean hasNestedResultMap(Result result) {
		if (result.one().resultMap().length() > 0 && result.many().resultMap().length() > 0) {
			throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
		}
		return result.one().resultMap().length() > 0 || result.many().resultMap().length() > 0;
	}

	private String nestedSelectId(Result result) {
		String nestedSelect = result.one().select();
		if (nestedSelect.length() < 1) {
			nestedSelect = result.many().select();
		}
		if (!nestedSelect.contains(StringPool.DOT)) {
			nestedSelect = type.getName() + StringPool.DOT + nestedSelect;
		}
		return nestedSelect;
	}

	private boolean isLazy(Result result) {
		boolean isLazy = configuration.isLazyLoadingEnabled();
		if (result.one().select().length() > 0 && FetchType.DEFAULT != result.one().fetchType()) {
			isLazy = result.one().fetchType() == FetchType.LAZY;
		} else if (result.many().select().length() > 0 && FetchType.DEFAULT != result.many().fetchType()) {
			isLazy = result.many().fetchType() == FetchType.LAZY;
		}
		return isLazy;
	}

	private boolean hasNestedSelect(Result result) {
		if (result.one().select().length() > 0 && result.many().select().length() > 0) {
			throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
		}
		return result.one().select().length() > 0 || result.many().select().length() > 0;
	}

	private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings) {
		for (Arg arg : args) {
			List<ResultFlag> flags = new ArrayList<>();
			flags.add(ResultFlag.CONSTRUCTOR);
			if (arg.id()) {
				flags.add(ResultFlag.ID);
			}
			@SuppressWarnings("unchecked")
			Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
					(arg.typeHandler() == UnknownTypeHandler.class ? null : arg.typeHandler());
			ResultMapping resultMapping = assistant.buildResultMapping(
					resultType,
					nullOrEmpty(arg.name()),
					nullOrEmpty(arg.column()),
					arg.javaType() == void.class ? null : arg.javaType(),
					arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(),
					nullOrEmpty(arg.select()),
					nullOrEmpty(arg.resultMap()),
					null,
					nullOrEmpty(arg.columnPrefix()),
					typeHandler,
					flags,
					null,
					null,
					false);
			resultMappings.add(resultMapping);
		}
	}

	private String nullOrEmpty(String value) {
		return value == null || value.trim().length() == 0 ? null : value;
	}

	private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
		String id = baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
		Class<?> resultTypeClass = selectKeyAnnotation.resultType();
		StatementType statementType = selectKeyAnnotation.statementType();
		String keyProperty = selectKeyAnnotation.keyProperty();
		String keyColumn = selectKeyAnnotation.keyColumn();
		boolean executeBefore = selectKeyAnnotation.before();

		// defaults
		boolean useCache = false;
		KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
		Integer fetchSize = null;
		Integer timeout = null;
		boolean flushCache = false;
		String parameterMap = null;
		String resultMap = null;
		ResultSetType resultSetTypeEnum = null;
		String databaseId = selectKeyAnnotation.databaseId().isEmpty() ? null : selectKeyAnnotation.databaseId();

		SqlSource sqlSource = buildSqlSource(selectKeyAnnotation, parameterTypeClass, languageDriver, null);
		SqlCommandType sqlCommandType = SqlCommandType.SELECT;

		assistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum,
				flushCache, useCache, false,
				keyGenerator, keyProperty, keyColumn, databaseId, languageDriver, null);

		id = assistant.applyCurrentNamespace(id, false);

		MappedStatement keyStatement = configuration.getMappedStatement(id, false);
		SelectKeyGenerator answer = new SelectKeyGenerator(keyStatement, executeBefore);
		configuration.addKeyGenerator(id, answer);
		return answer;
	}

	private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, LanguageDriver languageDriver,
									 Method method) {
		if (annotation instanceof Select) {
			return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver);
		} else if (annotation instanceof Update) {
			return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver);
		} else if (annotation instanceof Insert) {
			return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver);
		} else if (annotation instanceof Delete) {
			return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver);
		} else if (annotation instanceof SelectKey) {
			return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver);
		}
		return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
	}

	private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass,
												LanguageDriver languageDriver) {
		return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass);
	}

	@SafeVarargs
	private final Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
																   Class<? extends Annotation>... targetTypes) {
		return getAnnotationWrapper(method, errorIfNoMatch, Arrays.asList(targetTypes));
	}

	private Optional<AnnotationWrapper> getAnnotationWrapper(Method method, boolean errorIfNoMatch,
															 Collection<Class<? extends Annotation>> targetTypes) {
		String databaseId = configuration.getDatabaseId();
		Map<String, AnnotationWrapper> statementAnnotations = targetTypes.stream()
				.flatMap(x -> Arrays.stream(method.getAnnotationsByType(x))).map(AnnotationWrapper::new)
				.collect(Collectors.toMap(AnnotationWrapper::getDatabaseId, x -> x, (existing, duplicate) -> {
					throw new BuilderException(String.format("Detected conflicting annotations '%s' and '%s' on '%s'.",
							existing.getAnnotation(), duplicate.getAnnotation(),
							method.getDeclaringClass().getName() + StringPool.DOT + method.getName()));
				}));
		AnnotationWrapper annotationWrapper = null;
		if (databaseId != null) {
			annotationWrapper = statementAnnotations.get(databaseId);
		}
		if (annotationWrapper == null) {
			annotationWrapper = statementAnnotations.get(StringPool.EMPTY);
		}
		if (errorIfNoMatch && annotationWrapper == null && !statementAnnotations.isEmpty()) {
			// Annotations exist, but there is no matching one for the specified databaseId
			throw new BuilderException(
					String.format(
							"Could not find a statement annotation that correspond a current database or default statement on method '%s.%s'. Current database id is [%s].",
							method.getDeclaringClass().getName(), method.getName(), databaseId));
		}
		return Optional.ofNullable(annotationWrapper);
	}

	private class AnnotationWrapper {
		private final Annotation annotation;
		private final String databaseId;
		private final SqlCommandType sqlCommandType;

		AnnotationWrapper(Annotation annotation) {
			this.annotation = annotation;
			if (annotation instanceof Select) {
				databaseId = ((Select) annotation).databaseId();
				sqlCommandType = SqlCommandType.SELECT;
			} else if (annotation instanceof Update) {
				databaseId = ((Update) annotation).databaseId();
				sqlCommandType = SqlCommandType.UPDATE;
			} else if (annotation instanceof Insert) {
				databaseId = ((Insert) annotation).databaseId();
				sqlCommandType = SqlCommandType.INSERT;
			} else if (annotation instanceof Delete) {
				databaseId = ((Delete) annotation).databaseId();
				sqlCommandType = SqlCommandType.DELETE;
			} else if (annotation instanceof SelectProvider) {
				databaseId = ((SelectProvider) annotation).databaseId();
				sqlCommandType = SqlCommandType.SELECT;
			} else if (annotation instanceof UpdateProvider) {
				databaseId = ((UpdateProvider) annotation).databaseId();
				sqlCommandType = SqlCommandType.UPDATE;
			} else if (annotation instanceof InsertProvider) {
				databaseId = ((InsertProvider) annotation).databaseId();
				sqlCommandType = SqlCommandType.INSERT;
			} else if (annotation instanceof DeleteProvider) {
				databaseId = ((DeleteProvider) annotation).databaseId();
				sqlCommandType = SqlCommandType.DELETE;
			} else {
				sqlCommandType = SqlCommandType.UNKNOWN;
				if (annotation instanceof Options) {
					databaseId = ((Options) annotation).databaseId();
				} else if (annotation instanceof SelectKey) {
					databaseId = ((SelectKey) annotation).databaseId();
				} else {
					databaseId = StringPool.EMPTY;
				}
			}
		}

		public Annotation getAnnotation() {
			return annotation;
		}

		public String getDatabaseId() {
			return databaseId;
		}

		public SqlCommandType getSqlCommandType() {
			return sqlCommandType;
		}

	}

}
